/*	
	Copyright (c) 2006 Dancing Tortoise Software
 
	Permission is hereby granted, free of charge, to any person 
	obtaining a copy of this software and associated documentation
	files (the "Software"), to deal in the Software without 
	restriction, including without limitation the rights to use, 
	copy, modify, merge, publish, distribute, sublicense, and/or 
	sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following 
	conditions:

	The above copyright notice and this permission notice shall be
	included in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
	EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
	OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
	HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
	WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
	FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
	OTHER DEALINGS IN THE SOFTWARE.
	
	Simple Comic
	SimpleComicAppDelegate.m
*/



#import "SimpleComicAppDelegate.h"
#import <Sparkle/Sparkle.h>
#import <XADMaster/XADArchive.h>
#import "TSSTSessionWindowController.h"
#import "TSSTPage.h"
#import "TSSTManagedGroup.h"
#import "SS_PrefsController.h"
#import "TSSTCustomValueTransformers.h"
#import "NFIWindow.h"

@class TSSTHumanReadableIndex;


NSString * TSSTPageOrder =         @"pageOrder";
NSString * TSSTMaxAspectRatio =    @"aspectRatio";
NSString * TSSTPageZoomRate =      @"pageZoomRate";
NSString * TSSTFullscreen =        @"fullscreen";
NSString * TSSTTextEncoding =      @"textEncoding";
NSString * TSSTSavedSelection =    @"savedSelection";
NSString * TSSTThumbnailSize =     @"thumbnailSize";
NSString * TSSTTwoPageSpread =     @"twoPageSpread";
NSString * TSSTListVisible =       @"listVisible";
NSString * TSSTPageListDisplay =   @"pageListDisplay";
NSString * TSSTPageScaleOptions =  @"scaleOptions";
NSString * TSSTCorrectedHeight =   @"correctedPageHeight";
NSString * TSSTIgnoreDonation =    @"ignoreDonation";
NSString * TSSTScrollPosition =    @"scrollPosition";
NSString * TSSTConstrainScale =    @"constrainScale";
NSString * TSSTZoomLevel =         @"zoomLevel";
NSString * TSSTViewRotation =      @"rotation";
NSString * TSSTDebugMenu =         @"debugMenu";
NSString * TSSTBackgroundColor =   @"pageBackgroundColor";
NSString * TSSTBetaUpdateFeed =    @"betaUpdateFeed";
NSString * TSSTLogging =           @"logging";
NSString * TSSTSessionRestore =    @"sessionRestore";
NSString * TSSTScrollersVisible =  @"scrollersVisible";
NSString * TSSTAutoPageTurn =      @"autoPageTurn";
NSString * TSSTWindowAutoResize =  @"windowAutoResize";
NSString * TSSTLoupeDiameter =     @"loupeDiameter";
NSString * TSSTFullscreenBezel =   @"fullscreenBezel";



static NSMutableArray * TSSTComicImageTypes;
static NSArray        * TSSTComicArchiveTypes;
static NSArray        * TSSTComicAllTypes;



#pragma mark -
#pragma mark String Encoding Functions



/*  Taken from TextEdit and cleaned up.
    Sort using the equivalent Mac encoding as the major key. 
    Secondary key is the actual encoding value, which works well enough. 
    We treat Unicode encodings as special case, 
    putting them at top of the list.  */
static int encodingCompare(const void * firstPtr, const void * secondPtr)
{
    CFStringEncoding first = *(CFStringEncoding *)firstPtr;
    CFStringEncoding second = *(CFStringEncoding *)secondPtr;
    CFStringEncoding encodingOne = CFStringGetMostCompatibleMacStringEncoding(first);
    CFStringEncoding encodingTwo = CFStringGetMostCompatibleMacStringEncoding(second);
    // Pointers are identical
    if (first == second) 
    {
        return 0;
    }
    
    if(encodingOne == encodingTwo)
    {
        return (first >= second) ? 1 : -1;
    }
    
    if(encodingOne == kCFStringEncodingUnicode && encodingTwo == kCFStringEncodingUnicode)
    {
        return (encodingOne >= encodingTwo) ? -1 : 1;
    }
    else if(encodingOne == kCFStringEncodingUnicode)
    {
        return -1;
    }
    else if(encodingTwo == kCFStringEncodingUnicode)
    {
        return 1;
    }
    
    return (encodingOne >= encodingTwo) ? 1 : -1;
}



/*  Taken from TextEdit and cleaned up.
    Return a sorted array of all available string encodings. */
static NSArray * allAvailableStringEncodings(void)
{
    static NSMutableArray * allEncodings = nil;
    if (!allEncodings)
    {	// Build list of encodings, sorted, and including only those with human readable names
        const CFStringEncoding * cfEncodings = CFStringGetListOfAvailableEncodings();
        CFStringEncoding * tmp;
        int cnt, num = 0;
        while (cfEncodings[num] != kCFStringEncodingInvalidId) num++;	// Count
        tmp = malloc(sizeof(CFStringEncoding) * num);
        memcpy(tmp, cfEncodings, sizeof(CFStringEncoding) * num);       // Copy the list
        qsort(tmp, num, sizeof(CFStringEncoding), encodingCompare);     // Sort it
        allEncodings = [[NSMutableArray alloc] init];                   // Now put it in an NSArray
        for (cnt = 0; cnt < num; cnt++)
        {
            NSStringEncoding nsEncoding = CFStringConvertEncodingToNSStringEncoding(tmp[cnt]);
            if (nsEncoding && [NSString localizedNameOfStringEncoding:nsEncoding])
            {
                [allEncodings addObject: [NSNumber numberWithUnsignedInt: nsEncoding]];
            }
        }
        free(tmp);
    }
    return allEncodings;
}



@implementation SimpleComicAppDelegate



/*  Convenience method for adding metadata to the core data store.
    Used by Simple Comic to keep track of store versioning. */
+ (void)setMetadata:(NSString *)value forKey:(NSString *)key inStoreWithURL:(NSURL *)url inContext:(NSManagedObjectContext *)context
{
    NSPersistentStoreCoordinator *coordinator = [context persistentStoreCoordinator];
    id store = [coordinator persistentStoreForURL: url];
    NSMutableDictionary *metadata = [[coordinator metadataForPersistentStore: store] mutableCopy];
    [metadata setValue: value forKey: key];
    [coordinator setMetadata: metadata forPersistentStore: store];
    [metadata release];
}



/*  Sets up the user defaults and arrays of compatible file types. */
+ (void)initialize
{
    NSMutableDictionary* standardDefaults = [NSMutableDictionary dictionary];
	[standardDefaults setObject: [NSNumber numberWithBool: NO] forKey: TSSTPageOrder];
	[standardDefaults setObject: [NSNumber numberWithFloat: 1] forKey: TSSTMaxAspectRatio];
	[standardDefaults setObject: [NSNumber numberWithFloat: 0.1] forKey: TSSTPageZoomRate];
    [standardDefaults setObject: [NSNumber numberWithInt: 4] forKey: TSSTTextEncoding];
	[standardDefaults setObject: [NSNumber numberWithInt: 1] forKey: TSSTPageScaleOptions];
    [standardDefaults setObject: [NSNumber numberWithBool: NO] forKey: TSSTFullscreen];
    [standardDefaults setObject: [NSNumber numberWithInt: 100] forKey: TSSTThumbnailSize];
    [standardDefaults setObject: [NSNumber numberWithBool: YES] forKey: TSSTTwoPageSpread];
    [standardDefaults setObject: [NSNumber numberWithInt: 1200] forKey: TSSTCorrectedHeight];
    [standardDefaults setObject: [NSNumber numberWithBool: NO] forKey: TSSTIgnoreDonation];
    [standardDefaults setObject: [NSNumber numberWithBool: YES] forKey: TSSTConstrainScale];
    [standardDefaults setObject: [NSNumber numberWithBool: YES] forKey: TSSTScrollersVisible];
    [standardDefaults setObject: [NSNumber numberWithBool: YES] forKey: TSSTSessionRestore];
    [standardDefaults setObject: [NSNumber numberWithBool: NO] forKey: TSSTAutoPageTurn];
	[standardDefaults setObject: [NSArchiver archivedDataWithRootObject: [NSColor whiteColor]] forKey: TSSTBackgroundColor];
    [standardDefaults setObject: [NSNumber numberWithBool: NO] forKey: TSSTBetaUpdateFeed];
    [standardDefaults setObject: [NSNumber numberWithBool: NO] forKey: TSSTWindowAutoResize];
    [standardDefaults setObject: [NSNumber numberWithInt: 250] forKey: TSSTLoupeDiameter];
	[standardDefaults setObject: [NSNumber numberWithBool: YES] forKey: TSSTFullscreenBezel];

	NSUserDefaultsController * defaults = [NSUserDefaultsController sharedUserDefaultsController];
    [defaults setInitialValues: standardDefaults];
    id transformer = [[TSSTLastPathComponent new] autorelease];
	[NSValueTransformer setValueTransformer: transformer forName: @"TSSTLastPathComponent"];

    transformer = [[TSSTHumanReadableIndex new] autorelease];
	[NSValueTransformer setValueTransformer: transformer forName: @"TSSTHumanReadableIndex"];
    
	if([[[defaults values] valueForKey: TSSTBetaUpdateFeed] boolValue])
	{
		[[defaults values] setValue: @"http://www.dancingtortoise.com/simplecomic/simplecomic_beta.xml" forKey: SUFeedURLKey];
	}
    else
    {
		[[defaults values] setValue: @"http://www.dancingtortoise.com/simplecomic/simplecomic.xml" forKey: SUFeedURLKey];
    }
	
    TSSTComicArchiveTypes = [[NSArray arrayWithObjects: @"rar", @"cbr", @"zip", @"cbz", @"7z", @"lha", @"lzh", @"sit", @"tar", nil] retain];
    TSSTComicAllTypes = [[TSSTComicArchiveTypes arrayByAddingObjectsFromArray: [NSImage imageFileTypes]] retain];
    TSSTComicImageTypes = [NSMutableArray arrayWithArray: [NSImage imageFileTypes]];
    [TSSTComicImageTypes removeObject: @"pdf"];
    [TSSTComicImageTypes retain];
}



- (void) dealloc
{
	[[NSUserDefaultsController sharedUserDefaultsController] removeObserver: self];
    [sessions release];
    [preferences release];
    [super dealloc];
}



#pragma mark -
#pragma mark Application Delegate Methods



- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
	[[NSUserDefaultsController sharedUserDefaultsController] addObserver: self forKeyPath: @"values.betaUpdateFeed" options: 0 context: nil];
    delayAutosave = NO;
    launchInProgress = YES;
    filesFromLaunch = nil;
}



- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    [NSTimer scheduledTimerWithTimeInterval: 30.0 target: self selector: @selector(saveContext) userInfo: nil repeats: YES];
    sessions = [NSMutableArray new];
    preferences = nil;
    [self performSelector: @selector(sessionRelaunch)  withObject: nil afterDelay: 0.0];
    launchInProgress = NO;
}



- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
    int reply = NSTerminateNow;
    delayAutosave = NO;
    if(![self saveContext])
    {
        // Error handling wasn't implemented. Fall back to displaying a "quit anyway" panel.
        int alertReturn = NSRunAlertPanel(nil, @"Could not save changes while quitting. Quit anyway?" , @"Quit anyway", @"Cancel", nil);
        if (alertReturn == NSAlertAlternateReturn)
        {
            reply = NSTerminateCancel;	
        }
    }
    
    BOOL ignoreDonate = [[[[NSUserDefaultsController sharedUserDefaultsController] values] valueForKey: TSSTIgnoreDonation] boolValue];
    if(reply != NSTerminateCancel && !ignoreDonate)
    {
        if([NSApp runModalForWindow: donationPanel] != NSCancelButton)
        {
            [self donate: self];
        }
        [donationPanel close];
    }
    return reply;
}



- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window
{
    return [[self managedObjectContext] undoManager];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
					  ofObject:(id)object 
						change:(NSDictionary *)change 
					   context:(void *)context
{
	NSUserDefaultsController * defaults = [NSUserDefaultsController sharedUserDefaultsController];
	if([[[defaults values] valueForKey: TSSTBetaUpdateFeed] boolValue])
	{
		[[defaults values] setValue: @"http://www.dancingtortoise.com/simplecomic/simplecomic_beta.xml" forKey: SUFeedURLKey];
	}
    else
    {
		[[defaults values] setValue: @"http://www.dancingtortoise.com/simplecomic/simplecomic.xml" forKey: SUFeedURLKey];
    }
}


- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
{
    if(!launchInProgress)
    {
        [self newSessionWithFiles: filenames];
    }
    else
    {
        filesFromLaunch = [filenames retain];
    }
}



- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
    [self newSessionWithFiles: [NSArray arrayWithObject: filename]];
    return YES;
}


#pragma mark -
#pragma mark Core Data



- (NSManagedObjectModel *)managedObjectModel
{
    if (managedObjectModel) 
    {
        return managedObjectModel;
    }
	NSMutableSet *allBundles = [[NSMutableSet alloc] init];
	[allBundles addObject: [NSBundle mainBundle]];
	[allBundles addObjectsFromArray: [NSBundle allFrameworks]];
    managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles: [allBundles allObjects]] retain];
    [allBundles release];
    
    return managedObjectModel;
}



/*  Attempts to load Simple Comic's mSQL store from the application
    support folder.  If file does not already exist, one is created.  */
- (NSManagedObjectContext *) managedObjectContext
{
    NSError *error;
    NSString *applicationSupportFolder = nil;
    NSURL *url;
    NSFileManager *fileManager;
    NSPersistentStoreCoordinator *coordinator;
	NSDictionary *storeInfo;
    if (managedObjectContext)
	{
        return managedObjectContext;
    }
    
    fileManager = [NSFileManager defaultManager];
    applicationSupportFolder = [self applicationSupportFolder];
    if (![fileManager fileExistsAtPath: applicationSupportFolder isDirectory: nil] )
	{
        [fileManager createDirectoryAtPath: applicationSupportFolder attributes: nil];
    }
    
    url = [NSURL fileURLWithPath: [applicationSupportFolder stringByAppendingPathComponent: @"SimpleComic.sql"]];
	storeInfo = [NSPersistentStoreCoordinator metadataForPersistentStoreWithURL: url error: &error];
    
	if(![[storeInfo valueForKey: @"viewVersion"] isEqualToString: @"Version 24"])
	{
		[fileManager removeFileAtPath: [url path] handler: nil];
	}
    
    coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
    if ([coordinator addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: url options: nil error: &error])
	{
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator: coordinator];
		[SimpleComicAppDelegate setMetadata: @"Version 24" forKey: @"viewVersion" inStoreWithURL: url inContext: managedObjectContext];
    }
    else
	{
        [[NSApplication sharedApplication] presentError: error];
    }    
    [coordinator release];
    return managedObjectContext;
}



/*  Method creates an application support directory for Simpl Comic if one
    is does not already exist.
    @return The absolute path to Simple Comic's application support directory 
            as a string.  */
- (NSString *)applicationSupportFolder
{
    NSString *applicationSupportFolder = nil;
    FSRef foundRef;
    OSErr err = FSFindFolder(kUserDomain, kApplicationSupportFolderType, kDontCreateFolder, &foundRef);
    if (err != noErr)
	{
        NSRunAlertPanel(@"Alert", @"Can't find application support folder", @"Quit", nil, nil);
        [[NSApplication sharedApplication] terminate:self];
    } 
    else
	{
        unsigned char path[1024];
        FSRefMakePath(&foundRef, path, sizeof(path));
        applicationSupportFolder = [NSString stringWithUTF8String: (char *)path];
        applicationSupportFolder = [applicationSupportFolder stringByAppendingPathComponent: @"SimpleComic"];
    }
    return applicationSupportFolder;
}



- (BOOL)saveContext
{
    if(![[[[NSUserDefaultsController sharedUserDefaultsController] values] valueForKey: TSSTSessionRestore] boolValue])
    {
        return YES;
    }
    
    if(delayAutosave)
    {
        return NO;
    }
    
    [self savePrep];
    NSEnumerator * sessionEnumerator = [sessions objectEnumerator];
    TSSTSessionWindowController * controller;
    while (controller = [sessionEnumerator nextObject])
    {
        [controller updateSessionObject];
    }
    
    NSError *error;
    NSManagedObjectContext *context = [self managedObjectContext];
    BOOL saved = NO;
    if (context != nil)
	{
        if ([context commitEditing])
		{
            if (![context save: &error])
			{
				// This default error handling implementation should be changed to make sure the error presented includes application specific error recovery. For now, simply display 2 panels.
				[[NSApplication sharedApplication] presentError: error];
            }
            else 
            {
                saved = YES;
            }
        }
    }
    return saved;
}



/*  Prior to the the managedObjectContext being saved this method
    should be called.  It iterates through all Archive objects 
    in the context and deletes any that do not have any images associated
    with them. */
- (void)savePrep
{
    TSSTManagedGroup * archiveObject;
    NSEnumerator* archiveEnumerator = [[groupController content] objectEnumerator];
    while (archiveObject = [archiveEnumerator nextObject])
    {
        if(![[archiveObject valueForKey: @"images"] count])
        {
            if([[archiveObject valueForKey: @"nested"] boolValue])
            {
                [[NSFileManager defaultManager] removeFileAtPath: [archiveObject valueForKey: @"path"] handler: nil];
            }
            [[self managedObjectContext] deleteObject: (NSManagedObject *)archiveObject];
        }
    }
}



#pragma mark -
#pragma mark Session Managment



- (void)windowForSession:(NSManagedObject *)settings
{
    if([[settings valueForKey: @"images"] count] > 0)
    {
        TSSTSessionWindowController * comicWindow = [[TSSTSessionWindowController alloc] initWithSession: settings];
        [sessions addObject: comicWindow];
        [comicWindow release];
        [comicWindow showWindow: self];
    }
}



- (void)endSession:(TSSTSessionWindowController *)manager
{
    [[self managedObjectContext] deleteObject: (NSManagedObject *)[manager session]];
	[sessions removeObject: manager];
    NSEnumerator * windowEnumerator = [[NSApp windows] objectEnumerator];
    NSWindow * singleWindow;
    while (singleWindow = [windowEnumerator nextObject])
    {
        if([singleWindow class] == [NFIWindow class] && 
           [singleWindow isVisible])
        {
            [singleWindow makeKeyAndOrderFront: self];
        }
    }
}



- (void)sessionRelaunch
{    
    NSEnumerator * groupEnumerator = [[groupController content] objectEnumerator];
    NSManagedObject * session;
    TSSTManagedGroup * group;
    NSFileManager * manager = [NSFileManager defaultManager];
    NSDictionary * attributes;
    BOOL shouldRestore = [[[[NSUserDefaultsController sharedUserDefaultsController] values] valueForKey: TSSTSessionRestore] boolValue];
    
    while (group = [groupEnumerator nextObject])
    {
        attributes = [manager fileAttributesAtPath: [group valueForKey: @"path"] traverseLink: NO];
        if(!attributes || ![[group valueForKey: @"modified"] isEqualToDate: [attributes objectForKey: NSFileModificationDate]])
        {
            session = [[[group valueForKey: @"images"] anyObject] valueForKey: @"session"];
            [sessionController removeObject: session];
        }
    }
    
    NSEnumerator * sessionEnumerator = [[sessionController arrangedObjects] objectEnumerator];
    while (session = [sessionEnumerator nextObject])
    {
        if(!shouldRestore)
        {
            [[self managedObjectContext] deleteObject: session];
        }
        else
        {
            [self windowForSession: session];
        }
    }
    
    if(filesFromLaunch)
    {
        [self newSessionWithFiles: filesFromLaunch];
        [filesFromLaunch release];
        filesFromLaunch = nil;
    }
}



- (void)newSessionWithFiles:(NSArray *)files
{
    NSManagedObject * sessionDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Session" inManagedObjectContext: [self managedObjectContext]];
    NSDictionary * defaults = [[NSUserDefaultsController sharedUserDefaultsController] values];
    
    [sessionDescription setValue: [NSNumber numberWithBool: NO] forKey: TSSTFullscreen];
    [sessionDescription setValue: [defaults valueForKey: TSSTPageScaleOptions] forKey: TSSTPageScaleOptions];
    [sessionDescription setValue: [defaults valueForKey: TSSTPageOrder] forKey: TSSTPageOrder];
    [sessionDescription setValue: [defaults valueForKey: TSSTTwoPageSpread] forKey: TSSTTwoPageSpread];
    
    [self addFiles: files toSession: sessionDescription];
}



- (void)addFiles:(NSArray *)paths toSession:(NSManagedObject *)sessionDescription
{
    delayAutosave = YES;
    [[NSFileManager defaultManager] changeCurrentDirectoryPath: [[paths objectAtIndex: 0] stringByDeletingLastPathComponent]];
    NSMutableSet * pageSet = [NSMutableSet setWithSet: [sessionDescription valueForKey: @"images"]];
    NSSet * pages = [self pagesForFiles: paths];
    if([pages count] > 0)
    {
        [pageSet unionSet: pages];
        
        [sessionDescription setValue: pageSet forKey: @"images"];
        
        if(![sessionDescription valueForKey: @"position"])
        {
            [self windowForSession: sessionDescription];
        }
        delayAutosave = NO;
    }
}



#pragma mark -
#pragma mark Process Files



/*  Recursive entity creator */
- (NSSet *)pagesForFiles:(NSArray *)files
{
    if(!files)
    {
        return nil;
    }
    
    NSMutableSet * pageSet = [NSMutableSet set];
    NSFileManager * fileManager = [NSFileManager defaultManager];
	NSEnumerator * fileEnumerator = [files objectEnumerator];
	NSString * pathString;
    NSArray * nestedFiles;
    NSString * currentPath = [fileManager currentDirectoryPath];
	BOOL isDirectory, exists;
    NSManagedObject * imageDescription;
    TSSTManagedGroup * groupDescription;
    
    NSDictionary * attributes = [fileManager fileAttributesAtPath: currentPath traverseLink: NO];
    TSSTManagedGroup * superGroup = [NSEntityDescription insertNewObjectForEntityForName: @"ImageGroup" inManagedObjectContext: [self managedObjectContext]];
    [superGroup setValue: currentPath forKey: @"path"];
    [superGroup setValue: [attributes objectForKey: NSFileModificationDate] forKey: @"modified"];
    while(pathString = [fileEnumerator nextObject])
    {
        if(![[pathString substringWithRange: NSMakeRange(0,1)] isEqualToString: @"."])
        {
            if([[pathString pathComponents] count] == 1)
            {
                pathString = [currentPath stringByAppendingPathComponent: pathString];
            }
            else
            {
                [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: pathString]];
            }
            
            exists = [fileManager fileExistsAtPath: pathString isDirectory: &isDirectory];
            
            if(exists && isDirectory)
            {
                [fileManager changeCurrentDirectoryPath: pathString];
                nestedFiles = [fileManager directoryContentsAtPath: pathString];
                [pageSet unionSet: [self pagesForFiles: nestedFiles]];
            }
            else if(exists && [TSSTComicArchiveTypes containsObject: [[pathString pathExtension] lowercaseString]])
            {
                groupDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Archive" inManagedObjectContext: [self managedObjectContext]];
                [groupDescription setValue: pathString forKey: @"path"];
                [pageSet unionSet: [self nestedArchiveContents: (TSSTManagedArchive *)groupDescription]];
            }
            else if(exists && [TSSTComicImageTypes containsObject: [[pathString pathExtension] lowercaseString]])
            {
                imageDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Image" inManagedObjectContext: [self managedObjectContext]];
                [imageDescription setValue: pathString forKey: @"imagePath"];
                [imageDescription setValue: superGroup forKey: @"group"];
                [pageSet addObject: imageDescription];
            }
            else if(exists && [[[pathString pathExtension] lowercaseString] isEqualToString: @"pdf"])
            {
                groupDescription = [NSEntityDescription insertNewObjectForEntityForName: @"PDF" inManagedObjectContext: [self managedObjectContext]];
                [groupDescription setValue: pathString forKey: @"path"];
                attributes = [fileManager fileAttributesAtPath: pathString traverseLink: NO];
                [pageSet unionSet: [self pdfContents: (TSSTManagedPDF *)groupDescription]];
                [groupDescription setValue: [attributes objectForKey: NSFileModificationDate] forKey: @"modified"];
            }
        }
    }
    return pageSet;
}



- (NSSet *)nestedArchiveContents:(TSSTManagedArchive *)archive
{
    XADArchive * imageArchive = [archive valueForKey: @"instance"];
    if([imageArchive isEncrypted])
    {
        NSString * password = nil;
        NSData * testData = nil;
        do
        {
            password = [self passwordForArchiveWithPath: [archive valueForKey: @"path"]];
            [imageArchive setPassword: password];
            testData = [archive dataForPageIndex: 1];
        }while(password && !testData);
        
        [archive setValue: password forKey: @"password"];
        
        if(!testData)
        {
            return nil;
        }
    }
    
    NSFileManager * fileManager = [NSFileManager defaultManager];
    NSDictionary * attributes = [fileManager fileAttributesAtPath: [archive valueForKey: @"path"] traverseLink: NO];
    [archive setValue: [attributes objectForKey: NSFileModificationDate] forKey: @"modified"];
    
    int counter, archivedFilesCount = [imageArchive numberOfEntries];
    NSMutableSet * pageSet = [NSMutableSet set];
    NSManagedObject * imageDescription;
    TSSTManagedGroup * groupDescription;
    NSString * archivePath, * extension, * fileName;
    
    for (counter = 0; counter < archivedFilesCount; ++counter)
    {
        fileName = [imageArchive nameOfEntry: counter];
        
        if(!([fileName isEqualToString: @""] || [[fileName substringWithRange: NSMakeRange(0,1)] isEqualToString: @"."]))
        {
            extension = [[fileName pathExtension] lowercaseString];
            if([TSSTComicImageTypes containsObject: extension])
            {
                imageDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Image" inManagedObjectContext: [self managedObjectContext]];
                [imageDescription setValue: fileName forKey: @"imagePath"];
                [imageDescription setValue: [NSNumber numberWithInt: counter] forKey: @"index"];
                [imageDescription setValue: archive forKey: @"group"];
                [pageSet addObject: imageDescription];
            }
            else if([TSSTComicArchiveTypes containsObject: extension])
            {
                groupDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Archive" inManagedObjectContext: [self managedObjectContext]];
                archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent: fileName];
                int collision = 0;
                while([fileManager fileExistsAtPath: archivePath])
                {
                    ++collision;
                    fileName = (NSString *)[NSString stringWithFormat: @"%i-%@", collision, fileName];
                    archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent: fileName];
                }
                [imageArchive extractEntry: counter to: NSTemporaryDirectory() withName: fileName];
                [groupDescription setValue: archivePath forKey: @"path"];
                [groupDescription setValue: [NSNumber numberWithBool: YES] forKey: @"nested"];
                [pageSet unionSet: [self nestedArchiveContents: (TSSTManagedArchive *)groupDescription]];
            }
            else if([extension isEqualToString: @"pdf"])
            {
                groupDescription = [NSEntityDescription insertNewObjectForEntityForName: @"PDF" inManagedObjectContext: [self managedObjectContext]];
                archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent: fileName];
                int collision = 0;
                while([fileManager fileExistsAtPath: archivePath])
                {
                    ++collision;
                    fileName = [NSString stringWithFormat: @"%i-%@", collision, fileName];
                    archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent: fileName];
                }
                [imageArchive extractEntry: counter to: NSTemporaryDirectory() withName: fileName];
                [groupDescription setValue: archivePath forKey: @"path"];
                [groupDescription setValue: [NSNumber numberWithBool: YES] forKey: @"nested"];
                [pageSet unionSet: [self pdfContents: (TSSTManagedPDF *)groupDescription]];
            }
        }
    }
    
    return pageSet;
}



/*  Creates an image managedobject for every "page" in a pdf. */
- (NSSet *)pdfContents:(TSSTManagedPDF *)pdf
{
    NSPDFImageRep * rep = [pdf instance];
    NSManagedObject * imageDescription;
    NSMutableSet * pageSet = [NSMutableSet set];
    int imageCount = [rep pageCount];
    int pageNumber;
    for (pageNumber = 0; pageNumber < imageCount; ++pageNumber)
    {
        imageDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Image" inManagedObjectContext: [self managedObjectContext]];
        [imageDescription setValue: [NSString stringWithFormat: @"%i", pageNumber + 1] forKey: @"imagePath"];
        [imageDescription setValue: [NSNumber numberWithInt: pageNumber] forKey: @"index"];
        [imageDescription setValue: pdf forKey: @"group"];
        [pageSet addObject: imageDescription];
    }
    return pageSet;
}



#pragma mark -
#pragma mark Actions



// Launches open modal.
- (IBAction)addPages:(id)sender
{
    // Accessory view setup
    NSDictionary * defaults = [[NSUserDefaultsController sharedUserDefaultsController] values];
    [singlePage setState: [[defaults valueForKey: TSSTTwoPageSpread] boolValue]];
    [pageOrder selectCellWithTag: [[defaults valueForKey: TSSTPageOrder] intValue]];
    [pageScaling selectItemAtIndex: [[defaults valueForKey: TSSTPageScaleOptions] intValue]];
    
	// Creates a new modal.
	NSOpenPanel * addPagesModal = [NSOpenPanel openPanel];
    [addPagesModal setAccessoryView: prefsView];
	[addPagesModal setAllowsMultipleSelection: YES];
    [addPagesModal setCanChooseDirectories: YES];
    
	if([addPagesModal runModalForTypes: TSSTComicAllTypes] !=  NSCancelButton)
	{
        NSManagedObject * sessionDescription = [NSEntityDescription insertNewObjectForEntityForName: @"Session" inManagedObjectContext: [self managedObjectContext]];
        [sessionDescription setValue: [NSNumber numberWithBool: NO] forKey: TSSTFullscreen];
        [sessionDescription setValue: [NSNumber numberWithInt: [pageScaling indexOfSelectedItem]] forKey: TSSTPageScaleOptions];
        [sessionDescription setValue: [NSNumber numberWithInt: [pageOrder selectedTag]] forKey: TSSTPageOrder];
        [sessionDescription setValue: [NSNumber numberWithBool: [singlePage state]] forKey: TSSTTwoPageSpread];
        
        [self addFiles: [addPagesModal filenames] toSession: sessionDescription];
	}
}



/*  Kills the password and encoding modals if the OK button was  clicked. */
- (IBAction)modalOK:(id)sender
{
    [NSApp stopModalWithCode: NSOKButton]; 
}



/*  Kills the password and encoding modals if the Cancel button was clicked. */
- (IBAction)modalCancel:(id)sender
{
    [NSApp stopModalWithCode: NSCancelButton]; 
}



- (IBAction)openPreferences:(id)sender
{
    if(!preferences)
    {
        preferences = [[SS_PrefsController alloc] initWithPanesSearchPath: nil];
        
        // Set which panes are included, and their order.
        [preferences setPanesOrder: [NSArray arrayWithObjects:@"General", @"Advanced", nil]];
    }
    [preferences showPreferencesWindow];
}



- (IBAction)donate:(id)sender
{
    NSURL * donationPage = [NSURL URLWithString: @"http://www.dancingtortoise.com/simplecomic/"];
    [[NSWorkspace sharedWorkspace] openURL: donationPage];
}



#pragma mark -
#pragma mark Archive Encoding Handling



- (NSArray *)availableEncodings
{
    return availableEncodings;
}


- (void)setAvailableEncodings:(NSArray *)encodings
{
    if(availableEncodings != encodings)
    {
        [availableEncodings release];
        availableEncodings = [encodings retain];
    }
}



- (void)generateEncodingMenuTestedAgainst:(const char *)string
{
    NSEnumerator * encodingEnumerator = [allAvailableStringEncodings() objectEnumerator];
    NSNumber * encodingIdent;
    NSStringEncoding stringEncoding;
    NSMenuItem * encodingMenuItem;
    NSString * encodingName;
    NSMutableArray * encodings = [NSMutableArray array];
    NSString * testText;
    while(encodingIdent = [encodingEnumerator nextObject])
    {
        stringEncoding = [encodingIdent unsignedIntValue];
        testText = [[NSString alloc] initWithCString: string encoding: stringEncoding];
        if(testText)
        {
            encodingName = [NSString localizedNameOfStringEncoding: stringEncoding];
            encodingMenuItem = [[NSMenuItem alloc] initWithTitle: encodingName
                                                          action: nil
                                                   keyEquivalent: @""];
            [encodingMenuItem setRepresentedObject: encodingIdent];
            [encodingMenuItem setTag: stringEncoding];
            [encodings addObject: encodingMenuItem];
            [encodingMenuItem release];
        }
        [testText release];
    }
    [self setAvailableEncodings: encodings];
}



/*  Called when Simple Comic encounters a password protected
    archive.  Brings a password dialog forward. */
- (NSString*)passwordForArchiveWithPath:(NSString*)filename
{
    NSString* password = nil;
    if([NSApp runModalForWindow: passwordPanel] != NSCancelButton)
    {
        password = [passwordField stringValue];
    }
    [passwordPanel close];
    return password;
}



-(NSStringEncoding)archive: (XADArchive *)archive
           encodingForName: (const char *)bytes
                     guess: (NSStringEncoding)guess
                confidence: (float)confidence
{
    NSString * testText = [NSString stringWithCString: bytes encoding: guess];
    if(confidence < 0.8 || !testText)
    {
        [self generateEncodingMenuTestedAgainst: bytes];
        NSArray * encodingIdentifiers = [availableEncodings valueForKey: @"representedObject"];
        int index = [encodingIdentifiers indexOfObject: [NSNumber numberWithInt: guess]];
        
        if(index != NSNotFound)
        {
            [encodingController setSelectionIndex: index];
        }
        
        encodingTestString = bytes;
        [self testEncoding: self];
        if([NSApp runModalForWindow: encodingPanel] != NSCancelButton)
        {
            guess = [[[[encodingController selectedObjects] objectAtIndex: 0] representedObject] intValue];
        }
        [encodingPanel close];
        [archive setNameEncoding: guess];
    }
    
    return guess;
}



- (IBAction)testEncoding:(id)sender
{
    NSMenuItem * encodingMenuItem = [availableEncodings objectAtIndex: [encodingController selectionIndex]];
    NSString * testText = [NSString stringWithCString: encodingTestString encoding: [[encodingMenuItem representedObject] intValue]];
    if(!testText)
    {
        testText = @"invalid Selection";
    }
    [encodingTestField setStringValue: testText];
}



- (IBAction)actionStub:(id)sender
{
    
}



@end


